﻿namespace Jsl.FxCop
{
    using System;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.IO;
    using System.Reflection;
    using System.Resources;
    using System.Security;
    using System.Security.Permissions;
    using Microsoft.CSharp;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// A utility class that helps dynamically generate assemblies.
    /// </summary>
    /// <remarks>
    /// This can be used to build assemblies for FxCop tests that work at the assembly level.
    /// </remarks>
    public sealed class CompileHelper : IDisposable
    {
        private string assemblyFileName;
        private List<string> assemblyReferences = new List<string>();
        private CodeDomProvider codeProvider;
        private Dictionary<string, Action<ResourceWriter>> embeddedResources = new Dictionary<string, Action<ResourceWriter>>();
        private string lastAssemblyGenerated;
        private List<string> sources = new List<string>();

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="CompileHelper"/> class.
        /// </summary>
        public CompileHelper()
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Adds an assembly reference.
        /// </summary>
        /// <param name="assemblyReference">The assembly reference.</param>
        /// <returns>This instance.</returns>
        public CompileHelper AddAssemblyReference(string assemblyReference)
        {
            this.assemblyReferences.Add(assemblyReference);

            return this;
        }

        /// <summary>
        /// Adds the assembly reference.
        /// </summary>
        /// <param name="assembly">The assembly to reference.</param>
        /// <returns>This instance.</returns>
        public CompileHelper AddAssemblyReference(Assembly assembly)
        {
            if (assembly == null)
            {
                throw new ArgumentNullException("assembly");
            }

            this.assemblyReferences.Add(assembly.Location);

            return this;
        }

        /// <summary>
        /// Adds the assembly reference.
        /// </summary>
        /// <param name="type">The type in the assembly to add a reference to.</param>
        /// <returns>This instance.</returns>
        public CompileHelper AddAssemblyReference(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            return this.AddAssemblyReference(type.Assembly);
        }

        /// <summary>
        /// Adds an embedded resource.
        /// </summary>
        /// <param name="resourceName">Name of the embedded resource. Note that ".resources" is appended.</param>
        /// <param name="writerAction">Builds the embedded resource.</param>
        /// <returns>This instance.</returns>
        /// <remarks>
        /// Each resource is generated by calling the <paramref name="writerAction"/>.
        /// </remarks>
        public CompileHelper AddEmbeddedResource(string resourceName, Action<ResourceWriter> writerAction)
        {
            this.embeddedResources.Add(resourceName, writerAction);

            return this;
        }

        /// <summary>
        /// Adds the method code to the assembly.
        /// </summary>
        /// <param name="methodCode">The method code to add.</param>
        /// <returns>This instance.</returns>
        public CompileHelper AddMethod(string methodCode)
        {
            return this.AddSource(@"public class Class
{
" + methodCode + @"
}");
        }

        /// <summary>
        /// Adds source code to compile.
        /// </summary>
        /// <param name="source">The source code.</param>
        /// <returns>This instance.</returns>
        public CompileHelper AddSource(string source)
        {
            this.sources.Add(source);

            return this;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (this.codeProvider != null)
            {
                this.codeProvider.Dispose();
                this.codeProvider = null;
            }
        }

        /// <summary>
        /// Generates the assembly.
        /// </summary>
        /// <param name="debugMode"><c>true</c> if the assembly should be compiled in debug mode.</param>
        /// <returns>An <see cref="AssemblyNode"/> for the generated assembly.</returns>
        /// <exception cref="InvalidOperationException">A compiler error was found.</exception>
        [PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")]
        [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
        public AssemblyNode Generate(bool debugMode)
        {
            // If a file name hasn't been specified OR it has already been generated then build a random name for the assembly.
            if (string.IsNullOrEmpty(this.assemblyFileName) || (this.assemblyFileName == this.lastAssemblyGenerated))
            {
                this.assemblyFileName = GetTemporaryFileName() + ".dll";
            }

            // Compile the code into the dynamically built assembly.
            var options = new CompilerParameters()
            {
                GenerateExecutable = false,
                GenerateInMemory = false,
                IncludeDebugInformation = debugMode,
                OutputAssembly = this.assemblyFileName,
            };

            // Add in common assembly references.
            options.ReferencedAssemblies.Add("System.dll");

            // Add in any assembly references.
            foreach (string assemblyReference in this.assemblyReferences)
            {
                options.ReferencedAssemblies.Add(assemblyReference);
            }

            // Generate the embedded resource if any.
            foreach (var entry in this.embeddedResources)
            {
                string resourceFileName = Path.Combine(Environment.CurrentDirectory, entry.Key + ".resources");
                using (ResourceWriter writer = new ResourceWriter(resourceFileName))
                {
                    entry.Value(writer);
                    writer.Generate();
                }

                options.EmbeddedResources.Add(resourceFileName);
            }

            // If a code provider wasn't provided then default to CSharp.
            if (this.codeProvider == null)
            {
                this.codeProvider = new CSharpCodeProvider();
            }

            // Keep track of the last file name generated so we can generate a new name if called again.
            this.lastAssemblyGenerated = this.assemblyFileName;

            var compilerResults = this.codeProvider.CompileAssemblyFromSource(options, this.sources.ToArray());

            // If there were any compiler errors throw an exception.
            if (compilerResults.Errors.Count > 0)
            {
                throw new InvalidOperationException("Compile Error: " + compilerResults.Errors[0].ErrorText);
            }

            return AssemblyNodeHelper.GetAssemblyNode(this.assemblyFileName);
        }

        /// <summary>
        /// Sets the name of the assembly file.
        /// </summary>
        /// <param name="assemblyFileName">Name of the assembly file.</param>
        /// <returns>This instance.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "assemblyFileName", Justification = "By design.")]
        public CompileHelper SetAssemblyFileName(string assemblyFileName)
        {
            this.assemblyFileName = assemblyFileName;

            return this;
        }

        /// <summary>
        /// Sets the code provider.
        /// </summary>
        /// <param name="codeProvider">The code provider.</param>
        /// <returns>This instance.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "codeProvider", Justification = "By design.")]
        public CompileHelper SetCodeProvider(CodeDomProvider codeProvider)
        {
            this.codeProvider = codeProvider;

            return this;
        }
        #endregion Public Methods

        #region Private Methods
        private static string GetTemporaryFileName()
        {
            return Path.Combine(Environment.CurrentDirectory, "A" + Guid.NewGuid().ToString("N"));
        }
        #endregion Private Methods
    }
}